package me.hekr.iotos.api;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URL;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import lombok.Data;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import me.hekr.iotos.api.dto.AggDataQuery;
import me.hekr.iotos.api.dto.CloudSendMsgDTO;
import me.hekr.iotos.api.dto.DelTopoResp;
import me.hekr.iotos.api.dto.DevIdListReq;
import me.hekr.iotos.api.dto.DeviceAddReq;
import me.hekr.iotos.api.dto.DeviceAggPacketResp;
import me.hekr.iotos.api.dto.DeviceApiDTO;
import me.hekr.iotos.api.dto.DevicePacketResp;
import me.hekr.iotos.api.dto.DeviceStatusRes;
import me.hekr.iotos.api.dto.DeviceUpdateNameReq;
import me.hekr.iotos.api.dto.LoginRsp;
import me.hekr.iotos.api.dto.ModelProtocolDTO;
import me.hekr.iotos.api.dto.ParamValue;
import me.hekr.iotos.api.dto.ParamValueQuery;
import me.hekr.iotos.api.dto.ProductDTO;
import me.hekr.iotos.api.dto.Snapshot;
import me.hekr.iotos.api.dto.klink.AddTopoResp;
import me.hekr.iotos.api.dto.klink.KlinkResp;
import me.hekr.iotos.api.dto.klink.ModelData;
import me.hekr.iotos.api.dto.klink.TopoSub;
import me.hekr.iotos.api.enums.DeviceType;
import me.hekr.iotos.api.enums.ErrorCode;
import me.hekr.iotos.api.exception.IotException;
import me.hekr.iotos.api.service.RetrofitIotService;
import me.hekr.iotos.api.util.JsonUtil;
import me.hekr.iotos.api.util.Pagec;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.logging.HttpLoggingInterceptor;
import okhttp3.logging.HttpLoggingInterceptor.Level;
import org.apache.commons.lang3.StringUtils;
import retrofit2.Call;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.jackson.JacksonConverterFactory;
import retrofit2.http.Body;
import retrofit2.http.POST;

@Slf4j
public class IotClient {
  private static final String MAC_NAME = "HmacSHA1";
  private static final ObjectMapper objectMapper = new ObjectMapper();
  private static final Level logLevel = Level.BODY;

  static {
    objectMapper
        .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
        .configure(DeserializationFeature.FAIL_ON_TRAILING_TOKENS, true);
  }

  private final Retrofit retrofit;
  private final Builder builder;
  private final RetrofitIotService retrofitIotService;

  private IotClient(Builder builder) {
    this.builder = builder;
    if (StringUtils.isAnyBlank(builder.host, builder.ak, builder.as)) {
      throw new IllegalArgumentException("host, ak, as must not blank");
    }
    retrofit = createRetrofit();
    retrofitIotService = retrofit.create(RetrofitIotService.class);
  }

  public static Builder builder() {
    return new Builder();
  }

  @SneakyThrows
  public static <T> T execute(Call<T> call) {
    Response<T> response = call.execute();
    if (response.isSuccessful()) {
      T ret = response.body();
      if (ret instanceof KlinkResp) {
        int code = ((KlinkResp) ret).getCode();
        if (code != 0) {
          throw new IotException(code, ((KlinkResp) ret).getDesc());
        }
      }
      return ret;
    }

    String msg = response.toString();
    if (response.errorBody() != null) {
      String respStr = response.errorBody().string();
      msg += ", body: " + respStr;
      RetrofitIotService.log.error("调用失败，{}", msg);

      if (response.code() != 500) {
        IotErrorResponse result = JsonUtil.fromJson(respStr, IotErrorResponse.class);
        // 设备已经存在
        throw new IotException(result.getCode(), "IoTOS 接口调用失败," + msg);
      }
    }

    throw new IotException(ErrorCode.UNDEFINED.getCode(), "IoTOS 接口调用失败," + msg);
  }

  public <T> T register(Class<T> clazz) {
    return retrofit.create(clazz);
  }

  /**
   * 获取所有产品信息
   *
   * @return 产品列表
   */
  public List<ProductDTO> getAllProducts() {
    return Pagec.getAll(100, (page, size) -> retrofitIotService.getProduct(page, size));
  }

  /**
   * 创建设备
   *
   * @param addReq 设备信息
   * @return 添加的设备信息
   */
  public DeviceApiDTO createDevice(DeviceAddReq addReq) {
    return execute(retrofitIotService.createDevice(addReq));
  }

  /**
   * 获取设备信息
   *
   * @param pk pk
   * @param devId 设备ID
   * @return Device
   */
  public DeviceApiDTO getDevice(String pk, String devId) {
    return execute(retrofitIotService.getDevice(pk, devId));
  }

  public ModelProtocolDTO getProtocol(String pk) {
    return execute(retrofitIotService.getProtocol(pk));
  }

  @SneakyThrows
  private Retrofit createRetrofit() {
    String baseUrl = builder.host;
    if (!baseUrl.startsWith("http")) {
      baseUrl = "http://" + baseUrl;
    }

    if (!baseUrl.endsWith("/")) {
      baseUrl += "/";
    }

    // 获取前缀，可能没有前缀，获取到的只是/ ， 如果有前缀可能类似 /a/
    // 删除前导字符 /
    String prefix = StringUtils.stripEnd(new URL(baseUrl).getPath(), "/");

    HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
    interceptor.setLevel(logLevel);
    OkHttpClient client =
        new OkHttpClient.Builder()
            .addInterceptor(interceptor)
            .addInterceptor(
                chain -> {
                  String reqPath = chain.request().url().uri().getPath();
                  String realPath = reqPath;
                  // "" 代表没有前缀
                  if (StringUtils.isNotBlank(prefix)) {
                    realPath = StringUtils.removeStart(reqPath, prefix);
                  }

                  Request request =
                      chain
                          .request()
                          .newBuilder()
                          .addHeader(
                              "Authorization", getToken(realPath, builder.getAk(), builder.getAs()))
                          .addHeader("Content-Type", "application/json")
                          .build();
                  if (log.isDebugEnabled()) {
                    log.debug(
                        "request url: {}, request real path: {}",
                        request.url().toString(),
                        realPath);
                  }
                  return chain.proceed(request);
                })
            .callTimeout(10, TimeUnit.SECONDS)
            .connectTimeout(3, TimeUnit.SECONDS)
            .build();

    return new Retrofit.Builder()
        .client(client)
        .baseUrl(baseUrl)
        .addConverterFactory(JacksonConverterFactory.create(objectMapper))
        .build();
  }

  @SneakyThrows
  public String getToken(String path, String accessKey, String accessSecret) {
    String timestamp = String.valueOf(System.currentTimeMillis());
    String method = "SHA1";
    return assembleToken(path, timestamp, method, accessKey, accessSecret);
  }

  public byte[] hmacSHA1Encrypt(String encryptText, String encryptKey) throws Exception {
    byte[] data = encryptKey.getBytes();
    // 根据给定的字节数组构造一个密钥，第二参数指定一个密钥算法的名称。
    SecretKey secretKey = new SecretKeySpec(data, MAC_NAME);
    // 生成一个指定 Mac 算法 的 Mac 对象
    Mac mac = Mac.getInstance(MAC_NAME);
    // 用给定密钥初始化 Mac 对象
    mac.init(secretKey);
    byte[] text = encryptText.getBytes();
    // 完成 Mac 操作
    return mac.doFinal(text);
  }

  public String parseByte2HexStr(byte[] buf) {
    if (null == buf) {
      return null;
    }
    StringBuffer sb = new StringBuffer();
    for (int i = 0; i < buf.length; i++) {
      String hex = Integer.toHexString(buf[i] & 0xFF);
      if (hex.length() == 1) {
        hex = '0' + hex;
      }
      sb.append(hex.toUpperCase());
    }
    return sb.toString().toLowerCase();
  }

  public String assembleToken(
      String path, String timestamp, String method, String accessKey, String accessSecret)
      throws Exception {
    String encodePath = URLEncoder.encode(path, "UTF-8");
    String data = path + "\n" + timestamp + "\n" + method;
    String signature = parseByte2HexStr(hmacSHA1Encrypt(data, accessSecret));
    return "accessKey="
        + accessKey
        + "&path="
        + encodePath
        + "&timestamp="
        + timestamp
        + "&method="
        + method
        + "&sign="
        + signature;
  }

  /**
   * <a
   * href="http://hy.hekr.me/iot-docs-test/web/content/%E5%BA%94%E7%94%A8%E5%BC%80%E5%8F%91%E6%8C%87%E5%8D%97/%E8%8E%B7%E5%8F%96Token.html">获取鉴权信息</a>
   *
   * @return LoginRsp
   */
  LoginRsp getLoginToken() {
    return execute(retrofitIotService.getLoginToken());
  }

  /**
   * 获取产品列表
   *
   * <p><a
   * href="http://hy.hekr.me/iot-docs-test/web/content/%E5%BA%94%E7%94%A8%E5%BC%80%E5%8F%91%E6%8C%87%E5%8D%97/%E8%8E%B7%E5%8F%96%E4%BA%A7%E5%93%81%E5%88%97%E8%A1%A8.html">获取产品列表</a>
   *
   * @param page 当前页，从0开始
   * @param size 分页大小，最大100，最小1
   * @return 产品
   */
  public Pagec<ProductDTO> getProduct(int page, int size) {
    return execute(retrofitIotService.getProduct(page, size));
  }

  /**
   * 获取批次下所有设备
   *
   * <p><a
   * href="http://hy.hekr.me/iot-docs-test/web/content/%E5%BA%94%E7%94%A8%E5%BC%80%E5%8F%91%E6%8C%87%E5%8D%97/%E8%8E%B7%E5%8F%96%E6%89%B9%E6%AC%A1%E4%B8%8B%E6%89%80%E6%9C%89%E8%AE%BE%E5%A4%87.html">获取批次下所有设备</a>
   *
   * @param pk 产品PK
   * @param batchName 批次名称
   * @return 设备列表
   */
  public List<DeviceApiDTO> getBatchDevices(String pk, String batchName) {
    return execute(retrofitIotService.getBatchDevices(pk, batchName));
  }

  /**
   * 更改设备名称
   *
   * <p><a
   * href="http://hy.hekr.me/iot-docs-test/web/content/%E5%BA%94%E7%94%A8%E5%BC%80%E5%8F%91%E6%8C%87%E5%8D%97/%E6%9B%B4%E6%96%B0%E8%AE%BE%E5%A4%87.html">更改设备名称</a>
   *
   * @param req 设备信息（pk，devId，name）
   */
  public void updateName(DeviceUpdateNameReq req) {
    execute(retrofitIotService.updateName(req));
  }

  /**
   * 获取鉴权信息
   *
   * <p><a
   * href="http://hy.hekr.me/iot-docs-test/web/content/%E5%BA%94%E7%94%A8%E5%BC%80%E5%8F%91%E6%8C%87%E5%8D%97/%E8%8E%B7%E5%8F%96Token.html">获取鉴权信息</a>
   *
   * @param req 设备信息（pk，devId，name）
   * @return 鉴权信息
   */
  public List<DeviceStatusRes> getDeviceStatus(DevIdListReq req) {
    return execute(retrofitIotService.getDeviceStatus(req));
  }

  /**
   * 查询设备指标趋势
   *
   * <p><a
   * href="http://hy.hekr.me/iot-docs-test/web/content/%E5%BA%94%E7%94%A8%E5%BC%80%E5%8F%91%E6%8C%87%E5%8D%97/%E6%9F%A5%E8%AF%A2%E8%AE%BE%E5%A4%87%E6%8C%87%E6%A0%87%E8%B6%8B%E5%8A%BF.html">查询设备指标趋势</a>
   *
   * @param req 设备信息（pk，devId，name）
   * @return 设备信息列表
   */
  @POST("/api/deviceStat/kvlog/{pk}/{devId}")
  public List<ParamValue> getParamValue(@Body ParamValueQuery req) {
    return execute(retrofitIotService.getParamValue(req));
  }

  /**
   * 新增设备
   *
   * <p><a
   * href="http://hy.hekr.me/iot-docs-test/web/content/%E5%BA%94%E7%94%A8%E5%BC%80%E5%8F%91%E6%8C%87%E5%8D%97/%E6%96%B0%E5%A2%9E%E8%AE%BE%E5%A4%87.html">新增设备</a>
   *
   * @param reqDto 设备信息（pk，devId，name）
   * @return 设备信息
   */
  public DeviceApiDTO addDevice(DeviceAddReq reqDto) {
    return execute(retrofitIotService.addDevice(reqDto));
  }

  /**
   * 导入设备
   *
   * <p><a
   * href="http://hy.hekr.me/iot-docs-test/web/content/%E5%BA%94%E7%94%A8%E5%BC%80%E5%8F%91%E6%8C%87%E5%8D%97/%E5%AF%BC%E5%85%A5%E8%AE%BE%E5%A4%87.html">导入设备</a>
   *
   * @param devIdListReq （产品PK和设备ID列表）
   * @return 包含批次名称和添加数量
   */
  public Map<String, Object> batchAddDevices(DevIdListReq devIdListReq) {
    return execute(retrofitIotService.batchAddDevices(devIdListReq));
  }

  /**
   * 查询设备列表
   *
   * <p><a
   * href="http://hy.hekr.me/iot-docs-test/web/content/%E5%BA%94%E7%94%A8%E5%BC%80%E5%8F%91%E6%8C%87%E5%8D%97/%E6%9F%A5%E8%AF%A2%E8%AE%BE%E5%A4%87%E5%88%97%E8%A1%A8.html">查询设备列表</a>
   *
   * @param pk 产品PK
   * @param keyword 搜索关键词
   * @param page 当前页面，最大100，最小1
   * @param deviceType 节点类型（普通设备：GENERAL；中继设备：SWITCH；网关设备：GATEWAY；终端子设备：TERMINAL）
   * @param online 是否在线，true为在线
   * @param size 分页大小，默认10，
   * @return 设备信息
   */
  public Pagec<DeviceApiDTO> getDeviceList(
      String pk, String keyword, int page, DeviceType deviceType, Boolean online, int size) {
    return execute(retrofitIotService.getDeviceList(pk, keyword, page, deviceType, online, size));
  }

  /**
   * 查询设备详情
   *
   * <p><a
   * href="http://hy.hekr.me/iot-docs-test/web/content/%E5%BA%94%E7%94%A8%E5%BC%80%E5%8F%91%E6%8C%87%E5%8D%97/%E6%9F%A5%E8%AF%A2%E8%AE%BE%E5%A4%87%E8%AF%A6%E6%83%85.html">查询设备详情</a>
   *
   * @param pk 产品PK
   * @param devId 设备ID
   * @return 设备信息
   */
  public DeviceApiDTO deviceInfo(String pk, String devId) {
    return execute(retrofitIotService.deviceInfo(pk, devId));
  }

  /**
   * 查询设备影子
   *
   * <p><a
   * href="http://hy.hekr.me/iot-docs-test/web/content/%E5%BA%94%E7%94%A8%E5%BC%80%E5%8F%91%E6%8C%87%E5%8D%97/%E6%9F%A5%E8%AF%A2%E8%AE%BE%E5%A4%87%E5%BD%B1%E5%AD%90.html">查询设备影子</a>
   *
   * @param pk 产品PK
   * @param devId 设备ID
   * @return 设备影子
   */
  public Snapshot getDeviceSnapshot(String pk, String devId) {
    return execute(retrofitIotService.getDeviceSnapshot(pk, devId));
  }

  /**
   * 删除设备
   *
   * <p><a
   * href="http://hy.hekr.me/iot-docs-test/web/content/%E5%BA%94%E7%94%A8%E5%BC%80%E5%8F%91%E6%8C%87%E5%8D%97/%E5%88%A0%E9%99%A4%E8%AE%BE%E5%A4%87.html">删除设备</a>
   *
   * @param pk 产品PK
   * @param devId 设备ID
   * @param delSnapshot 是否删除设备影子
   */
  public void delDevice(String pk, String devId, boolean delSnapshot) {
    execute(retrofitIotService.delDevice(pk, devId, delSnapshot));
  }

  /**
   * 查询历史上下行数
   *
   * <p><a
   * href="http://hy.hekr.me/iot-docs-test/web/content/%E5%BA%94%E7%94%A8%E5%BC%80%E5%8F%91%E6%8C%87%E5%8D%97/%E6%9F%A5%E8%AF%A2%E5%8E%86%E5%8F%B2%E4%B8%8A%E4%B8%8B%E8%A1%8C%E6%95%B0%E6%8D%AE.html">查询历史上下行数据</a>
   *
   * @param pk 产品PK
   * @param devId 设备ID
   * @param startTime 开始事件
   * @param endTime 结束事件
   * @param action 事件
   * @param page 当前页 从0开始
   * @param size 分页大小，最大100，最小1
   * @return 上下行数据信息
   */
  public List<DevicePacketResp> getDeviceHistoryData(
      String pk, String devId, Long startTime, Long endTime, String action, int page, int size) {
    return execute(
        retrofitIotService.getDeviceHistoryData(pk, devId, startTime, endTime, action, page, size));
  }

  /**
   * 下发控制命令
   *
   * <p><a
   * href="http://hy.hekr.me/iot-docs-test/web/content/%E5%BA%94%E7%94%A8%E5%BC%80%E5%8F%91%E6%8C%87%E5%8D%97/%E4%B8%8B%E5%8F%91%E6%8E%A7%E5%88%B6%E5%91%BD%E4%BB%A4.html">下发控制命令</a>
   *
   * @param pk 产品PK
   * @param devId 设备ID
   * @param data （cmd 命令标识符 ，params 参数值）
   * @return 指令信息
   */
  public KlinkResp deviceCloudSend(String pk, String devId, ModelData data) {
    return execute(retrofitIotService.deviceCloudSend(pk, devId, data));
  }

  /**
   * 查询命令状态
   *
   * <p><a
   * href="http://hy.hekr.me/iot-docs-test/web/content/%E5%BA%94%E7%94%A8%E5%BC%80%E5%8F%91%E6%8C%87%E5%8D%97/%E6%9F%A5%E8%AF%A2%E5%91%BD%E4%BB%A4%E7%8A%B6%E6%80%81.html">查询命令状态</a>
   *
   * @param messageId 所查询命令的ID
   * @return 命令信息
   */
  public CloudSendMsgDTO cloudSendMsgInfo(String messageId) {
    return execute(retrofitIotService.cloudSendMsgInfo(messageId));
  }

  /**
   * 查询历史控制命令
   *
   * <p><a
   * href="http://hy.hekr.me/iot-docs-test/web/content/%E5%BA%94%E7%94%A8%E5%BC%80%E5%8F%91%E6%8C%87%E5%8D%97/%E6%9F%A5%E8%AF%A2%E5%8E%86%E5%8F%B2%E6%8E%A7%E5%88%B6%E5%91%BD%E4%BB%A4.html">查询历史控制命令</a>
   *
   * @param page 当前页面，最小0
   * @param size 分页大小，最大100，最小0
   * @param pk 产品PK
   * @param devId 设备ID
   * @param startTime 查询开始时间
   * @param endTime 查询结束事件
   * @return 命令信息
   */
  public Pagec<CloudSendMsgDTO> cloudSendMsgList(
      Integer page, Integer size, String pk, String devId, Long startTime, Long endTime) {
    return execute(retrofitIotService.cloudSendMsgList(page, size, pk, devId, startTime, endTime));
  }

  /**
   * 网关添加子设备
   *
   * <p><a
   * href="http://hy.hekr.me/iot-docs-test/web/content/%E5%BA%94%E7%94%A8%E5%BC%80%E5%8F%91%E6%8C%87%E5%8D%97/%E7%BD%91%E5%85%B3%E6%B7%BB%E5%8A%A0%E5%AD%90%E8%AE%BE%E5%A4%87.html">网关添加子设备</a>
   *
   * @param pk 产品PK
   * @param devId 设备ID
   * @param sub 子设备
   * @return 添加的子设备信息
   */
  public AddTopoResp addTopo(String pk, String devId, TopoSub sub) {
    return execute(retrofitIotService.addTopo(pk, devId, sub));
  }

  /**
   * 网关删除子设备
   *
   * @param pk 产品PK
   * @param devId 设备ID
   * @param sub 子设备
   * @return 删除的子设备信息
   */
  public DelTopoResp delTopo(String pk, String devId, TopoSub sub) {
    return execute(retrofitIotService.delTopo(pk, devId, sub));
  }

  /**
   * 查询设备指标聚合
   *
   * <p><a
   * href="http://hy.hekr.me/iot-docs-test/web/content/%E5%BA%94%E7%94%A8%E5%BC%80%E5%8F%91%E6%8C%87%E5%8D%97/%E6%9F%A5%E8%AF%A2%E8%AE%BE%E5%A4%87%E6%8C%87%E6%A0%87%E8%81%9A%E5%90%88.html">查询设备指标聚合</a>
   *
   * @param query 查询条件
   * @return DeviceAggPacketResp
   */
  public DeviceAggPacketResp getDeviceAggData(AggDataQuery query) {
    Map<String, Object> params = new HashMap<>();
    params.put("key", query.getKey());
    params.put("span", query.getSpan());
    params.put("startTime", query.getStartTime());
    params.put("endTime", query.getEndTime());
    return execute(retrofitIotService.getDeviceAggData(query.getPk(), query.getDevId(), params));
  }

  /** */
  @Data
  public static class Builder {
    private String host;
    private String ak;
    private String as;

    private Level level = Level.HEADERS;

    /**
     * 地址，如 http://10.1.1.100:8081/ 可以带前缀路径，比如 http://10.1.1.200:7003/aa/
     *
     * @param host 地址
     * @return Builder
     */
    public Builder host(String host) {
      this.host = host;
      return this;
    }

    /**
     * accessKey
     *
     * @param ak accessKey
     * @return Builder
     */
    public Builder ak(String ak) {
      this.ak = ak;
      return this;
    }

    /**
     * accessSecret
     *
     * @param as accessSecret
     * @return Builder
     */
    public Builder as(String as) {
      this.as = as;
      return this;
    }

    public IotClient build() {
      return new IotClient(this);
    }
  }
}
